function Generator() {}

/**
 * Hashes of the properties to be excluded from MapOptions/MarkerOptions.
 */
Generator.MARKER_EXCEPTIONS = {content: 1};
Generator.MAP_EXCEPTIONS = {
  height: 1, width: 1, bounds: 1, useBounds: 1,
  mapTypeId: function(id) {
    return 'google.maps.MapTypeId.' + id.toUpperCase();
  }
};

/**
 * Generates HTML/JS based on data obtained from the map wizard.
 * @param {Object} representation of map, marker collections, markers, etc.
 * @return {string} the html
 */
Generator.prototype.generate = function(data) {
  this.indent_ = '  ';

  // Fail gracefully if not all state is available yet.
  var code = 'No preview available yet.';
  try {
    var templateData = {
      mapCode: this.renderCode(data),
      markerCollections: this.formatMarkerCollections(data.markerCollections),
      height: data.map.height,
      width: data.map.width,
      misc: data.misc
    };
    code = wizard.preview(templateData);
  } catch (e) {}
  return code;
};

/**
 * Renders the JavaScript code needed for the user to create their custom map.
 * @param {Object} The wizard state.
 * @return {string} The Javascript code.
 */
Generator.prototype.renderCode = function(data) {
  return this.renderMapCode(data.map);
};

/**
 * Renders the JavaScript code needed to initialize the user's map.
 * @param {Object} The wizard map state.
 * @return {String} The JavaScript map code.
 */
Generator.prototype.renderMapCode = function(mapData) {
  var code = '  // Map initialization.\n' +
      '  map = new google.maps.Map(document.getElementById("map-canvas"), {' +
      '\n    // Custom map options.\n' +
      this.formatObjectContents(mapData, Generator.MAP_EXCEPTIONS) + '\n' +
      '  });\n';
  if (mapData.useBounds) {
    var bounds = mapData.bounds;
    code +=
        '  map.fitBounds(new google.maps.LatLngBounds(\n' +
        '      new google.maps.LatLng' + bounds.getSouthWest() + ',\n' +
        '      new google.maps.LatLng' + bounds.getNorthEast() + '));\n';
  }
  return code;
};

/**
 * Returns a string representation of the marker collections to insert into the
 * template.
 * @param {Object} markerCollections
 * @return {string}
 */
Generator.prototype.formatMarkerCollections = function(markerCollections) {
  var collections = [];
  for (var c in markerCollections) {
    var markers = markerCollections[c].getMarkers();
    var markerArray = [];
    for (var i = 0; i < markers.length; ++i) {
      var marker = markers[i];
      var position = marker.getPosition();
      markerArray.push({
        title: marker.getTitle(),
        content: marker.content,
        position: marker.getPosition()
      });
    }
    if (markerArray.length) {
      collections.push({
        name: c,
        options: markerCollections[c].getOptions(),
        markers: markerArray
      });
    }
  }

  return collections.length ? this.formatValue(collections) : null;
};

/**
 * Combines the properties of two objects.
 * @param object1 The first object.
 * @param object2 The second object.
 * @return The combined object.
 */
Generator.prototype.joinObjects = function(object1, object2) {
  var newObject = {};
  for (var prop in object1) {
    newObject[prop] = object1[prop];
  }
  for (var prop in object2) {
    newObject[prop] = object2[prop];
  }
  return newObject;
}

/**
 * Creates a string representation of the contents of an object.
 * @param {string} data The object literal to be formatted.
 * @param {Object} opt_exceptions A hash of the object properties to be
 *     excluded.
 * @return {string} The string representation of the object.
 */
Generator.prototype.formatObject = function(data, opt_exceptions) {
  var buffer = '{\n' +
      this.formatObjectContents(data, opt_exceptions) + '\n' +
      this.indent_ + '}';
  return buffer;
};

/**
 * Creates a string representation of the contents of an object without the
 * enclosing parentheses.
 * @param {string} data The object literal to be formatted.
 * @param {Object} opt_exceptions A hash of the object properties to be
 *     excluded.
 * @return {string} The string representation of the object's contents. 
 */
Generator.prototype.formatObjectContents = function(data, opt_exceptions) {
  var arrLines = [];
  var oldIndent = this.indent_;
  var newIndent = this.indent_ += '  ';
  var exceptions = opt_exceptions || {};
  for (var prop in data) {
    var special = exceptions[prop];
    var value = data[prop];
    if (!special) {
      var formattedValue = this.formatValue(value);
      if (formattedValue != null && formattedValue != undefined) {
        arrLines.push(newIndent + prop + ': ' + formattedValue);
      }
    } else if (value && typeof special == 'function') {
      arrLines.push(newIndent + prop + ': ' + special(value));
    }
  }
  this.indent_ = oldIndent;
  return arrLines.join(',\n');
};

/**
 * @param {string} value The string to be formatted for use in JS.
 * @return {string} The formatted string.
 */
Generator.prototype.formatString = function(value) {
  // Escapes \ and " and then wraps the string in ".
  return '"' + value.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
};

/**
 * Formats a value as how it would be represented in a string.
 * @param The value to be formatted.
 * @param The formatted value.
 */
Generator.prototype.formatValue = function(value) {
  var buffer;
  if (value instanceof Array) {
    var arrLines = [];
    for (var i = 0; i < value.length; i++) {
      arrLines.push(this.formatValue(value[i]));
    }
    buffer = '[' + arrLines.join(', ') + ']';
  } else {
    switch(typeof value) {
      case 'object':
        if (value instanceof google.maps.LatLng) {
          buffer = 'new google.maps.LatLng' + value;
        } else {
          buffer = this.formatObject(value);
        }
        break;
      case 'function':
        buffer = null;
      case 'string':
        buffer = this.formatString(value);
        break;
      default:
        buffer = value;
    }
  }
  return buffer;
};
